﻿using System;
using System.Globalization;
using System.Web;
using System.Web.UI;

namespace Shumisen.Web.Form.Modules
{
	// TODO Pageに属性をつけてCSRFチェックを行わないことを指定できるようにする。
	// TODO クッキー名やhiddenの名前を設定により変更可能とする。
	public class AntiCsrfModule : IHttpModule
	{
		public void Init(HttpApplication context)
		{
			context.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
			context.PreSendRequestHeaders += OnPreSendRequestHeaders;
		}

		public void Dispose()
		{
		}

		protected virtual void OnPreRequestHandlerExecute(Object sender, EventArgs e)
		{
			var context = HttpContext.Current;
			var page = context.Handler as Page;
			if (page == null)
			{
				return;
			}
			DetectCsrf();
			page.Load += OnLoad;
			page.PreRender += OnPreRender;
		}

		protected virtual void DetectCsrf()
		{
			var context = HttpContext.Current;
			if (context.Request.IsGetMethod())
			{
				return;
			}
			var cookieCsrfToken = GetCookieCsrfToken();
			if (string.IsNullOrEmpty(cookieCsrfToken))
			{
				// TODO 適切な例外
				throw new InvalidOperationException("CSRF");
			}
			var serializedCsrfToken = context.Request.Form[HiddenFieldNames.CsrfToken];
			if (string.IsNullOrEmpty(serializedCsrfToken))
			{
				// TODO 適切な例外
				throw new InvalidOperationException("CSRF");
			}
			// TODO デシリアライズの必要性と方法の検討
			var stateFormatter = new ObjectStateFormatter();
			string formCsrfToken;
			try
			{
				formCsrfToken = stateFormatter.Deserialize(serializedCsrfToken) as string;
			} catch(Exception e)
			{
				// TODO 適切な例外
				throw new InvalidOperationException("CSRF");
			}
			if (cookieCsrfToken != formCsrfToken)
			{
				// TODO 適切な例外
				throw new InvalidOperationException("CSRF");
			}
		}

		protected virtual void OnLoad(Object sender, EventArgs e)
		{
			var page = (Page) sender;
			if (page.IsCallback && page.Request.IsGetMethod())
			{
				// TODO 適切な例外
				throw new InvalidOperationException("CSRF");
			}
		}

		protected virtual void OnPreRender(Object sender, EventArgs e)
		{
			var page = sender as Page;
			if (page == null || page.Form == null)
			{
				return;
			}
			var csrfToken = GetCookieCsrfToken() ?? GenerateAndSaveCsrfToken();
			// TODO シリアライズの必要性と方法の検討
			var stateFormatter = new ObjectStateFormatter();
			page.ClientScript.RegisterHiddenField(HiddenFieldNames.CsrfToken, stateFormatter.Serialize(csrfToken));
		}

		protected virtual string GetCookieCsrfToken()
		{
			var context = HttpContext.Current;
			var csrfCookie = context.Request.Cookies[CookieNames.CsrfCookie];
			return csrfCookie != null ? csrfCookie.Value : null;
		}

		protected virtual string GenerateAndSaveCsrfToken()
		{
			// TODO 適切な生成方法
			var csrfToken = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);
			var context = HttpContext.Current;
			context.Items[HttpContextItemNames.CsrfToken] = csrfToken;
			return csrfToken;
		}

		protected virtual void OnPreSendRequestHeaders(Object sender, EventArgs e)
		{
			var context = HttpContext.Current;
			var csrfToken = context.Items[HttpContextItemNames.CsrfToken];
			if (csrfToken == null)
			{
				return;
			}
			var csrfCookie = new HttpCookie(CookieNames.CsrfCookie)
			                 {
			                 	Value = csrfToken.ToString(),
			                 	HttpOnly = true
			                 };
			context.Response.Cookies.Add(csrfCookie);
		}
	}
}